using System;
using System.Collections.Generic;
using NobleTech.Products.CreatureKingdom.Utils;

namespace NobleTech.Products.CreatureKingdom
{
    /// <summary>
    /// The world is made up of Quadrants
    /// A Quadrant is either empty or contains an Organism and four sub-Quadrants.
    /// An Organism has a link to the Quadrant it is in, which is maintained by the Quadrant.
    /// Each Quadrant handles the contained Organism's Moved event to put it into the right Quadrant.
    /// An organism stored in a Quadrant is always at least as big as those stored in the sub-Quadrants.
    /// </summary>
    internal class Quadrant : IQuadrant
    {
        private RectangleD area;
        public RectangleD Area { get { return area; } }

        /// <summary>
        /// The Quadrant that this Quadrant is in
        /// </summary>
        protected IQuadrant Parent;

        private IQuadrant[] quadrants;
        protected IQuadrant this[bool isLeft, bool isTop]
        {
            get { return quadrants[(isLeft ? 0 : 1) | (isTop ? 0 : 2)]; }
            set
            {
                int index = (isLeft ? 0 : 1) | (isTop ? 0 : 2);
                IQuadrant quadrant = quadrants[index];
                if (quadrant != null)
                {
                    quadrant.OrganismGrown -= new ChildQuadrantEventHandler(quadrant_OrganismGrown);
                }
                quadrant = quadrants[index] = value;
                if (quadrant != null)
                {
                    quadrant.OrganismGrown += new ChildQuadrantEventHandler(quadrant_OrganismGrown);
                }
                else
                    throw new ArgumentNullException("value", "Tried to set sub-Quadrant to null");
            }
        }

        private Organism organism = null;
        protected Organism Organism
        {
            get { return organism; }
            set
            {
                if (organism != null)
                {
                    // Remove the link from the current organism to this Quadrant
                    if (organism.Quadrant != this)
                        throw new InvalidOperationException("Tried to remove a link to an Organism that is not linked to this Quadrant");
                    organism.Moved -= new EventHandler(organism_Moved);
                    organism.Grown -= new EventHandler(organism_Grown);
                    organism.Quadrant = null;
                }
                organism = value;
                // New value can be null to indicate an empty Quadrant
                if (organism != null)
                {
                    // Establish a link from the new organism to this Quadrant
                    if (organism.Quadrant != null)
                        throw new InvalidOperationException("Tried to link an Organism to this Quadrant that was already linked to a Quadrant");
                    organism.Quadrant = this;
                    organism.Moved += new EventHandler(organism_Moved);
                    organism.Grown += new EventHandler(organism_Grown);
                }
            }
        }

        public bool IsEmpty { get { return Organism == null; } }
        public Organism LargestOrganism { get { return organism; } }

        public Quadrant(RectangleD area, IQuadrant parent)
        {
            this.area = area;
            if (parent == null)
                throw new ArgumentNullException("parent");
            this.Parent = parent;
        }

        public Quadrant(RectangleD area, Organism organism, IQuadrant parent)
            : this(area, parent)
        {
            InitialiseQuadrants();

            if (organism == null)
                throw new ArgumentNullException("organism");
            Organism = organism;
        }

        /// <summary>
        /// Create a Quadrant to wrap a child Quadrant, extending it out in a given direction
        /// </summary>
        /// <param name="child">The child Quadrant to wrap</param>
        /// <param name="extendLeft">Whether to extend left rather than right</param>
        /// <param name="extendUp">Whether to extend up rather than down</param>
        /// <param name="parent">The parent for this new Quadrant</param>
        /// <remarks>
        /// child must have its parent changed to the newly created Quadrant
        /// </remarks>
        protected Quadrant(Quadrant child, bool extendLeft, bool extendUp, IQuadrant parent)
            : this((RectangleD)child.Area.Extend(
                extendLeft ? child.Area.Width : 0, extendUp ? child.Area.Height : 0,
                extendLeft ? 0 : child.Area.Width, extendUp ? 0 : child.Area.Height), parent)
        {
            // Use the current size of child and make the others fit up to it
            PointD middle = new PointD(extendLeft ? child.Area.Left : child.Area.Right,
                extendUp ? child.Area.Top : child.Area.Bottom);
            InitialiseQuadrants(middle);
            // Replace the correct one with child
            this[!extendLeft, !extendUp] = child;
            // Get an organism from the child to put in this Quadrant
            Organism = child.RemoveLargestOrganism();
        }

        private void InitialiseQuadrants()
        {
            InitialiseQuadrants(Area.Centre);
        }

        private void InitialiseQuadrants(PointD middle)
        {
            quadrants = new IQuadrant[4];
            // Create all the empty quadrants
            this[true, true] = new Quadrant(new RectangleD(Area.LeftTop, middle), this);
            this[false, true] = new Quadrant(new RectangleD(Area.RightTop, middle), this);
            this[true, false] = new Quadrant(new RectangleD(Area.LeftBottom, middle), this);
            this[false, false] = new Quadrant(new RectangleD(Area.RightBottom, middle), this);
        }

        internal IEnumerable<Organism> GetOrganisms(RectangleD bounds, double minSize)
        {
            return GetSmallestQuadrantContaining(bounds).GetContainedOrganisms(bounds, minSize);
        }

        public IEnumerable<Organism> GetContainedOrganisms(RectangleD bounds, double minSize)
        {
            if (IsEmpty)
                // No organisms to return
                yield break;
            // TODO: Somehow need to see organisms that are centred outside
            // of the bounds but are big enough that they are inside the bounds
            // This would need to be done at a higher level than this function
            if (Organism.Size < minSize)
                yield break;
            if (RectangleD.Intersection(Area, bounds).IsEmpty)
                yield break;
            if (bounds.Contains(Organism.Location))
                yield return Organism;
            foreach (IQuadrant quadrant in quadrants)
                foreach (Organism org in quadrant.GetContainedOrganisms(bounds, minSize))
                    yield return org;
        }

        public void AddOrganism(Organism organism)
        {
            if (organism == Organism)
                // Already here
                throw new InvalidOperationException("Tried to add organism to Quadrant that already contained it");
            if (Area.Contains(organism.Location))
            {
                if (IsEmpty)
                {
                    Organism = organism;
                    InitialiseQuadrants();
                }
                else if (organism.Size > Organism.Size)
                {
                    // Replace current Organism with organism
                    Organism current = Organism;
                    Organism = organism;
                    // Now add current Organism to the appropriate sub-Quadrant
                    // AddOrganism will return this and nothing needs changing in Parent
                    AddOrganism(current);
                    // Might need to swap the new Organism up to the parent
                    OnOrganismGrown();
                }
                else
                {
                    // Put organism into appropriate sub-Quadrant
                    bool inTop = organism.Location.Y < Area.Centre.Y,
                        inLeft = organism.Location.X < Area.Centre.X;
                    this[inLeft, inTop].AddOrganism(organism);
                }
            }
            else
            {
                Parent.AddOrganism(organism);
            }
        }

        /// <summary>
        /// Remove the Organism in this Quadrant
        /// </summary>
        public Organism RemoveLargestOrganism()
        {
            Organism myOrganism = Organism;
            if (IsEmpty)
                throw new InvalidOperationException("Tried to remove Organism from an empty Quadrant");
            // Bring the largest of the child Organisms up to this level
            IQuadrant largestQuad = null;
            for (int quad = 0; quad < 4; ++quad)
            {
                Organism child = quadrants[quad].LargestOrganism;
                if (child != null &&
                    (largestQuad == null || child.Size > largestQuad.LargestOrganism.Size))
                {
                    largestQuad = quadrants[quad];
                }
            }
            if (largestQuad == null)
            {
                // Remove link to this from Organism
                Organism = null;
                // Don't bother removing child Quadrants
            }
            else
                Organism = largestQuad.RemoveLargestOrganism();
            return myOrganism;
        }

        public IQuadrant GetSmallestQuadrantContaining(RectangleD bounds)
        {
            if (!Area.Contains(bounds))
                return Parent.GetSmallestQuadrantContaining(bounds);
            if (!IsEmpty)
                foreach (IQuadrant quadrant in quadrants)
                    if (quadrant.Area.Contains(bounds))
                        return quadrant.GetSmallestQuadrantContaining(bounds);
            return this;
        }

        private bool SubQuadrantsEmpty
        {
            get
            {
                foreach (IQuadrant quadrant in quadrants)
                    if (!quadrant.IsEmpty)
                        return false;
                return true;
            }
        }

        public IQuadrant Extend(PointD towards)
        {
            if (!(Parent is TopQuadrant))
                throw new InvalidOperationException("Attempt to Extend a Quadrant that isn't a child of a TopQuadrant");
            if (!IsEmpty && !SubQuadrantsEmpty)
            {
                // Can't change the size of this Quadrant as it has sub-Quadrants,
                // which have fixed size
                Quadrant wrapper = new Quadrant(this, towards.X < Area.Centre.X,
                    towards.Y < Area.Centre.Y, Parent);
                Parent = wrapper;
                return wrapper;
            }
            // All the children are empty, just expand our area
            RectangleD area = (RectangleD)Area.ExtendAround(towards);
            area.Width = area.Height = Math.Max(area.Width, area.Height);
            this.area = area;
            if (!IsEmpty)
                // We need to replace the existing empty Quadrants with ones that match our new area
                InitialiseQuadrants();
            return this;
        }

        #region Turn Steps

        public void GetMove()
        {
            if (Organism != null)
            {
                Organism.GetMove();
                foreach (IQuadrant quadrant in quadrants)
                    quadrant.GetMove();
            }
        }

        public void UpdateInfo()
        {
            if (Organism != null)
            {
                Organism.UpdateInfo();
                foreach (IQuadrant quadrant in quadrants)
                    quadrant.UpdateInfo();
            }
        }

        public void DoInteraction()
        {
            if (Organism != null)
            {
                Organism.DoInteraction();
                foreach (IQuadrant quadrant in quadrants)
                    quadrant.DoInteraction();
            }
        }

        public void DoMove()
        {
            if (Organism != null)
            {
                Organism.DoMove();
                foreach (IQuadrant quadrant in quadrants)
                    quadrant.DoMove();
            }
        }

        public void DoLifeEvents()
        {
            if (Organism != null)
            {
                Organism.DoLifeEvents();
                foreach (IQuadrant quadrant in quadrants)
                    quadrant.DoLifeEvents();
            }
        }

        #endregion

        public event ChildQuadrantEventHandler OrganismGrown;
        protected void OnOrganismGrown()
        {
            ChildQuadrantEventHandler organismGrown = OrganismGrown;
            if (organismGrown != null)
                organismGrown(this, Parent);
        }

        /// <summary>
        /// Called when organism has moved, potentially out of this Quadrant
        /// </summary>
        private void organism_Moved(object sender, EventArgs e)
        {
            if (sender != Organism)
                throw new ApplicationException("Got a Moved event from an Organism other than the one linked to this Quadrant");
            if (Area.Contains(Organism.Location))
                // Still in this Quadrant
                return;
            // Remove and hold on to my Organism
            Organism myOrganism = RemoveLargestOrganism();
            // Add myOrganism into correct Quadrant
            AddOrganism(myOrganism);
        }

        private void organism_Grown(object sender, EventArgs e)
        {
            OnOrganismGrown();
        }

        private void quadrant_OrganismGrown(IQuadrant child, IQuadrant parent)
        {
            if (parent != this)
                throw new ApplicationException("Emptied fired by a child of a Quadrant with the parent set to another Quadrant");
            // Only need to do anything if child Organism is larger than one here
            if (child.LargestOrganism.Size > LargestOrganism.Size)
            {
                // Hold on to my current Organism
                Organism myOrganism = Organism;
                // Remove largest organism from child and store it here
                Organism = child.RemoveLargestOrganism();
                // Put my current Organism into the appropriate child
                AddOrganism(myOrganism);
            }
        }

        /// <summary>
        /// ToString returns a representation of the Quadrant useful for debugging
        /// </summary>
        /// <returns>A string representation of the Quadrant</returns>
        public override string ToString()
        {
            return string.Format("Quadrant: Area = {{{0}}}; IsEmpty = {2}", Area, IsEmpty);
        }
    }
}
